﻿using System;
using System.Linq;
using System.IO;
using System.Reflection;
using System.Threading;

namespace Citi.Scheduler.WindowsServices
{
    [Serializable]
    public class AssemblyRunner : MarshalByRefObject
    {
        public override object InitializeLifetimeService()
        {
            return null;
        }

        public static string ValidateDll(string assemblyPath, string className)
        {
            if (!File.Exists(assemblyPath))
                throw new FileNotFoundException("File not found", assemblyPath);

            AppDomain domain = null;
            try
            {
                var runner = LoadAssemblyRunner(assemblyPath, ref domain);
                return runner.LoadAndValidateDll(assemblyPath, className);
            }
            finally
            {
                if (domain != null)
                    AppDomain.Unload(domain);
            }
        }

        private static AssemblyRunner LoadAssemblyRunner(string assemblyPath, ref AppDomain domain)
        {
            //create a new domain with a config
            var setup = new AppDomainSetup { ApplicationBase = Path.GetDirectoryName(assemblyPath) };
            //directory where the dll and dependencies are
            //try to load a configuration file
            if (File.Exists(string.Concat(assemblyPath, ".config")))
                setup.ConfigurationFile = string.Concat(assemblyPath, ".config");
            setup.ApplicationName = "AssemblyRunner";
            //Create the new domain 
            domain = AppDomain.CreateDomain("AssemblyRunner", null, setup);

            var runner =
                (AssemblyRunner)
                domain.CreateInstanceFromAndUnwrap(Assembly.GetExecutingAssembly().Location,
                                                   typeof(AssemblyRunner).FullName);
            return runner;
        }

        private static void UnloadDomain()
        {
            try
            {
                if (Domain != null)
                    AppDomain.Unload(Domain);
            }
            catch { }
        }

        public static void ClearRunner()
        {
            UnloadDomain();
            //Runner = null;
            Domain = null;
            //ObjectClass = null;
        }

        static AppDomain Domain = null;
        public static AssemblyRunner Runner = null;
        public static void LoadDllAndExecuteMethod(string assemblyPath, string className, string method, object[] parameters)
        {
            if (!File.Exists(assemblyPath))
                throw new FileNotFoundException("File not found", assemblyPath);

            //AppDomain domain = null;
            //AssemblyRunner runner = null;
            try
            {
                Runner = LoadAssemblyRunner(assemblyPath, ref Domain);
                Runner.ExecuteMethod(assemblyPath, className, method, parameters);
                //return runner;
            }
            finally
            {
                //if (domain != null)
                //    AppDomain.Unload(domain);

            }
        }

        private object objectClass { get; set; }

        public void ExecuteMethod(string filePath, string className, string method, object[] parameters)
        {
            if (!File.Exists(filePath)) return;

            var dll = LoadDll(filePath);

            if (objectClass == null)
            {
                var executionClass = dll.GetExportedTypes()
                                            .FirstOrDefault(x => x.GetInterfaces().Any(y => y.FullName.EndsWith("IExecutableRoutine")) && x.Name == className);

                var classType = dll.GetType(executionClass.FullName, true);
                objectClass = Activator.CreateInstance(dll.FullName, classType.FullName).Unwrap();
            }

            var run = objectClass.GetType().GetMethod(method);

            run.Invoke(objectClass, parameters);
        }

        private string LoadAndValidateDll(string filePath, string className)
        {
            if (!File.Exists(filePath)) return null;

            var dll = LoadDll(filePath);

            var referencedAssemblies = dll.GetReferencedAssemblies().ToList();
            var routineBaseDll = referencedAssemblies.FirstOrDefault(x => x.FullName.Contains("Citi.RoutineBase"));

            if (routineBaseDll == null)
                throw new Exception("DLL Citi.RoutineBase was not found.");

            var executionClass = dll.GetExportedTypes()
                                    .FirstOrDefault(
                                        x =>
                                        x.GetInterfaces().Any(y => y.FullName.EndsWith("IExecutableRoutine")) &&
                                        x.Name == className);

            if (executionClass == null)
                throw new Exception(string.Format("There's no class named {0}", className));

            return routineBaseDll.Version.ToString();
        }

        private Assembly LoadDll(string filePath)
        {
            string location = Path.GetDirectoryName(filePath);

            AppDomain.CurrentDomain.AssemblyResolve +=
                (sender, args) =>
                {
                    var findName = args.Name;
                    var simpleName = new AssemblyName(findName).Name;
                    Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("pt-BR");
                    var assemblyPath = string.Concat(Path.Combine(location, simpleName) + ".dll");
                    return File.Exists(assemblyPath) ? Assembly.LoadFrom(assemblyPath) : null;
                };

            //load the assembly into bytes and load it
            var assemblyBytes = File.ReadAllBytes(filePath);
            return Assembly.Load(assemblyBytes);
        }
    }
}